SHAP (Shapley Additive Explanation) 是一种借鉴了博恋 论思想的事后解释方法, SHAP 通过计算模型中各个特征的边际贡献来衡是各个特征的影响大小,进而对黑盒模型进行解释。该边际贡献在 SHAP 中称为 Shapley Value, 最开始由 2012 年诺贝尔经济学奖的获得者 Lloyd Shapley 于 1938 年提出, 用于解决合作博亦论中的分配均衡问题。此外, 由于 Shapley Value本身的计算过程非常耗时, 后续业内又发展了多种 Shapley Value 的近似计算方法, 以提高其计算速度。这些近似算法主 要是依据我们想要解释的黑盒模型的特点而提出的, 根据近似算法的不同思路, 我们可以将 SHAP 分为不同的类型, 有针对树模型的 TreeSHAP、针对神经网络的 DeepSHAP,还有与模 型无关的 Kernel SHAP。
在博弈论中, 多个参与者的一次合作可以带来一次产出。 对于某个参与者(如张三)而言, 张三的边际贡献=他参与此次合作的产出-他不参与此次合作的产出。张三的边际贡献越大, 说明他对这次合作的影响越大, 我们可以通过这样的方法计算每个参与者的边际贡献, 以衡量每个参与者对这次合作产出的影响大小。
这里将上述博孪论思想引人 SHAP 中:将合作视作建模, 将合作中的参与者视作建模时的特征。对于某个样本而言, 假设其特征有三个特征 $\left(x_1, x_2, x_3,\right), x_1$ 的边际贡献 $=x_1$ 人模时模型的预测结果 - $x_1$ 不入模时模荊的预测结果。由于 $x_1$ 是否入模对应的特征组合情况有很多, 比如, $f\left(x_1, x_2\right)-f\left(x_2\right)$ 、 $f\left(x_1, x_3\right)-f\left(x_3\right) 、 f\left(x_1, x_2, x_3\right)-f\left(x_2, x_3\right)$, 每种特征组合下的边际贡献结果都需要考虑到, 因此我们可以先将每种特征组合情况下 $x_1$ 的边际贡献及该组合出现的概率都计算出来, 然后计算不同特征组合下边际贡献的期望值, 最后综合衡量 $x_1$ 的重要性。这里的边际贡献期望值就是 Shaply Value, 我们可以通过这样的方式计算每个样本中每个特征的 Shaply Value, 以衡量该样本的各个特征对模型结果的影响, 从而对黑盒模型的预测结果进行解释。
SHAP 的全称是 Shapley Additive Explanation, 其中, Additive 表明该方法使用加性的方式来描述各个特征对模型结果的影响。对于单个样本 $x$, 事后解释模型 $g$ 的表达形式为:
$$
g(x)=\phi_0+\sum_{i=1}^M \phi_i
$$
其中, $M$ 是黑盒模型 $f$ 中特征的数量; $\phi_0$ 是 $f$ 关于所有样本 预测值的平均值, 也称为 base value; $\phi_i$ 是需要计算的第 $i$ 个特征的 Shapley Value, 也是整个 SHAP 方法的核心所在。此外, 事后解释模型 $g$, 还需要满足以下列举的三个性质。
1) 性质 1:局部保真性 (local accuracy), 即事后解释模型 $g$ 对单个样本的预测值与黑盒模型对单个样本的预测值要相等, 也就是 $g(x)=f(x)$ 。
2) 性质 2:缺失性(missingness), 如果单个样本中存在缺失值, 则该样本的缺失特征对事后解释模型 $g$ 没有影响, $\phi_i$ 为 0 。
3) 性质 3:一致性 (consistency),当复杂模型发生变化时, 如从随机森林变为 XGBoost, 那么对单个样本而言, 特征的 $\phi_i$ 会随该特征在新模型中贡献的变化而变化。
Lyold Shapley 在 1938 年的一䈁论文中证明了满足定义 $g(x)=\phi_0+\sum_{i=1}^M \phi_i$ 和上述三个性质的 $\phi_i$ 有唯一解,具体证明过 程在此不赘述。
根据事后解释模型 $g$ 的局部保真性 (local accuracy), 对于单个样本 $x$, 有 $g(x)=f(x)$, 所以可以使用黑盒模型的预溳 结果 $f(x)$ 替换式 中的 $g(x)$, 即: $$ f(x)=\phi_0+\sum_{i=1}^M \phi_i $$ 根据上式可以看到, 黑盒模型的预测结果 $f(x)$ 可 以分解为各个特征的 $\phi_i$ 之和, $\phi_i$ 反映了各项特征对 $f(x)$ 的影响大小, 因而可以实现对黑盒模型预测结果的解释。
式中, $\phi_i$ 的计算公式为 $\phi_i=\sum_{S \subseteq\left(M \backslash x_i\right\rangle} \frac{|S| !(|M|-|S|-1) !}{|M| !}\left\{f\left(x_{S \cup(i\}}\right)-f\left(x_S\right)\right\}$
上式是一个期望值, 表示在不同特征组合下, $x_i$ 人模与 不人模时模型结果的变化情况。其中, $M$ 表示特征全集;S表 示 $\left\{M \backslash x_i\right\}$ 的特征子集, $S$ 的取值有多种情况,分别对应了不同的特征组合; $f\left(x_{S \cup(i)}\right)$ 和 $f\left(x_S\right)$ 分别表示各种特征组合下 $x_i$ 人 模与不人模时,模型的输出结果; $\frac{|S| !(|M|-|S|-1) !}{|M| !}$ 表示各种特征组合对应的概率, "||"表示集合的元素个数, “!" 表示阶乘。下面对该概率计算公式进行推导,在计算特征 $x_i$ 的边际贡献时, 各种特征组合出现的概率计算过程如下。
1) 先从特征全集 $M$ 中抽取 $x_i$, 此时的概率为: $\frac{1}{|M|}$ 。
2) 再从剩余的特征集合中抽取子集 $S$, 此时的概率为: $\frac{1}{\mathrm{C}_{|M|-1}^{|S|}}=\frac{|S| !(|M|-|S|-1) !}{(|M|-1) !}$
3) 将步骤 1 和步骤 2 的概率相乘, 乘积就是我们想要计算的 侮种特征组合出现的概率, 即 $$\frac{1}{M} \times \frac{|S| !(|M|-|S|-1) !}{(|M|-1) !}=\frac{|S| !(|M|-|S|-1) !}{|M| !}$$
下面通过一个例子来介绍计算过程。假设黑盒模型为 $f$, 某个样本共有 3 个特征 $(x, y, z$, ), 这里特征全集 $M=x, y, z$, 其剔除 $x$ 后的特征子集 $S$ 的取值共有 $\{\varnothing\}$ 、 $\{y\} 、\{z\} 、\{y, z\}$ 这 4 种情况, 下面按照之前式子的方式计算特 征 $x_1$ 的 Shapley Value, 具体结果如下表所示。
SHAP 方法的核心在于如何计算每个特征的 Shapley Value $\left(\phi_i\right)$, 要求在计算 $\phi_i$ 时, 对特征全集 $M$ 的所有子集 $S$ (这里的 $S$ 包含 $x_i$ 和 $M$ ) 都计算 $f(S)$ 。对于一个含有 $M$ 个元素的集合, 其子集的数量为 $2^{|\mathrm{M}|}$, 因而我们需要计算的 $f(S)$ 的次数也是 $2^{|M|}$ 次, 时间复杂度达到了指数级。
为了提高该模型的计算效率, 后续有一些学者提出了 $\phi_i$ 的各种近似计算方法。 根据这些算法的不同, 我们可以将 SHAP 分为两大类:一种是 与模型无关的算法, 这类算法并不是针对某个特定的模型而提出的, 基本上所有的黑盒模型都可以使用,代表算法是 Kernel SHAP; 另一种是根据不同黑盒模型的特点而专门提出的算法, 代表算法有针对神经网络的 DeepSHAP 和针对树模型的 TreeSHAP。这些算法中, 使用最广泛的是 TreeSHAP, 下面主要介绍 TreeSHAP 的计算思路。
TreeSHAP 是一种结合树模型的特点提出的实现 Shapley $\operatorname{Value}\left(\phi_i\right)$ 的高效算 法, 可用于解释随机森林、XBGoost、 LightGBM 等树模型的黑盒模型, 主要由 Scott M. Lundberg 和 $\mathrm{Su}-\mathrm{In}$ Lee 于 2018 年提出。在正式介绍 TreeSHAP 的算法之前, 我们先以回归树为例, 以先前介绍的理论公式展示 $\phi_i$ 的计算过程, 再通过案例的计算思路归纳 TreeSHAP 的算法。 假设有一棵如图所示的回归树。
点, 右下角的 $q$ 表示该节点的样本量, 该回归树的总样本量为 10 。某样本特征取值为 $x=180, y=100, z=300$, 模型的预测结果为 $f(x, y, z)=20$, 现在来计算各个特征的 $\phi_i$ 。 首先, 在特征全集 $(x, y, z)$ 的各个特征子集下, 计算树模型的输出结果, 具体如下表所示。 接下来,计算$\phi_x,\phi_y,\phi_z$
import shap
shap.initjs()
from sklearn.datasets import load_breast_cancer,load_iris
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
data = load_iris()
df = pd.DataFrame(data['data'],columns=data['feature_names'])
df['target'] = data['target']
X,y = df.iloc[:,:-1],df['target']
X_train,X_test,y_train,y_test = train_test_split(X,y,train_size=0.7)
rf = RandomForestClassifier(n_estimators=500)
rf.fit(X_train,y_train)
explainer = shap.TreeExplainer(rf,link='logit')
shap_values = explainer.shap_values(X_test.iloc[31,:])
shap.force_plot(explainer.expected_value[1],shap_values[1],X_test.iloc[31,:],link='logit')
shap_values_all = explainer.shap_values(X_test)
shap.summary_plot(shap_values_all,X_test,plot_type='bar')
SHAP 方法有三大优点。首先, SHAP 是一个理论完备的解释方法, 即完整的博恋理论。其中的对称性、可加性、有效 性等公理使得该解释变得更加合理。相比之下, LIME 只是假设机器学习模型在局部有线性关系, 宑没在坚实的理论来解释 为什么可以这么做。其次, SHAP 方法公平地分配了样本中每 个特征的贡献值, 最终解释了单个样本模型预测值与平均模型预测值之间的差异。这也是 SHAP 与 LIME 的不同之处, LIME 并不能保证模型的预测值可以公平地分配给每一个特 征。最后, Shapley Value 可以有不同的对比解释, 其既可以 解释单个样本的模型预测值与平均模型预测值之间的差异, 也可以解释单个样本的模型预测值与另一个样本的模型预测值之间的差异。
当然, SHAP 方法的缺点主要在 Shapley Value 的计算方法上。第一, 计算耗时过长, 虽然 TreeSHAP 的时间复杂度只有多项式级别, 但这仅仅是针对树模型而言的。对于其他黑盒 模型, 需要使用其他近似算法时 (如 Kernel SHAP), Shapley Value 的计算复杂度仍然不低。第二, 当特征之间存在相关性 时, 有些近似算法的效果会变差, 如 Kernel SHAP 的近似算 法要求特征之间要互相独立, 然而大多数情况并不满足这一条 件(如用户的收人和学历往往存在很大的相关性), 这会导致我们得到的关于黑盒模型的 “解释性结果” 不够准确。
参考资料:
import shap
shap.initjs()
from sklearn.datasets import load_breast_cancer,load_iris
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
data = load_iris()
df = pd.DataFrame(data['data'],columns=data['feature_names'])
df['target'] = data['target']
X,y = df.iloc[:,:-1],df['target']
X_train,X_test,y_train,y_test = train_test_split(X,y,train_size=0.7)
rf = RandomForestClassifier(n_estimators=500)
rf.fit(X_train,y_train)
explainer = shap.TreeExplainer(rf,link='logit')
shap_values = explainer.shap_values(X_test.iloc[31,:])
shap.force_plot(explainer.expected_value[1],shap_values[1],X_test.iloc[31,:],link='logit')
shap_values_all = explainer.shap_values(X_test)
shap.summary_plot(shap_values_all,X_test,plot_type='bar')